﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;

namespace MadLibrary.Web
{
    /// <summary>
    /// Holds informations about parsed node.
    /// </summary>
    public class ParsedNode
    {
        /// <summary>
        /// Gets or sets position on which was current node found.
        /// </summary>
        public int Position { get; set; }
        /// <summary>
        /// Gets or sets content of current node.
        /// </summary>
        public string Content { get; set; }
        /// <summary>
        /// Gets name of current parsed node.
        /// </summary>
        public string Name 
        { 
            get 
            {
                Regex r = new Regex("([A-Za-z]+)");
                Match m = r.Match(this.Content);
                string res = m.Groups[1].Value;
                if (res.StartsWith("/"))
                    res = res.Remove(0, 1);
                if (res.EndsWith("/"))
                    res = res.Remove(res.Length - 1);
                return res; 
            } 
        }
        /// <summary>
        /// Gets or sets type of current node.
        /// </summary>
        public NodeType Type { get; set; }
        /// <summary>
        /// Creates new empty parsed node.
        /// </summary>
        public ParsedNode()
        {
            this.Position = 0;
            this.Content = "";
            this.Type = NodeType.Unknown;
        }
        /// <summary>
        /// Creates new parsed node with specified position and specified content.
        /// </summary>
        /// <param name="Position">Specifies position on which was new node found.</param>
        /// <param name="Content">Specifies content of new node.</param>
        public ParsedNode(int Position, string Content)
        {
            this.Position = Position;
            this.Content = Content;
            this.Type = this.GetType();
        }
        /// <summary>
        /// Gets NodeType of current node.
        /// </summary>
        /// <returns>NodeType of current node.</returns>
        private new NodeType GetType()
        {
            if (this.Content.EndsWith("/"))
                return NodeType.OneLine;
            else if (this.Content.StartsWith("/"))
                return NodeType.Ending;
            else
                return NodeType.Starting;
        }
    }
    /// <summary>
    /// Manages HTML document.
    /// </summary>
    public class HtmlDocument : WebFile
    {
        /// <summary>
        /// Holds used positions.
        /// </summary>
        private List<int> UsedPositions = new List<int>();
        /// <summary>
        /// Gets or sets HtmlElement collection of current document.
        /// </summary>
        public HtmlElements Elements { get; set; }
        /// <summary>
        /// Creates new empty HTML document.
        /// </summary>
        public HtmlDocument() : base()
        {
            this.Elements = new HtmlElements();
        }
        /// <summary>
        /// Creates new HTML document with specified name.
        /// </summary>
        /// <param name="Name">Specifies name of new document.</param>
        public HtmlDocument(string Name) : base(Name)
        {
            this.Elements = new HtmlElements();
        }
        /// <summary>
        /// Loads and parses HTML code from file on specified path.
        /// </summary>
        /// <param name="Path">Specifies path of file which should be loaded.</param>
        public void LoadFile(string Path)
        {
            this.Name = new FileInfo(Path).Name;
            this.Parse(File.ReadAllText(Path));
        }
        /// <summary>
        /// Loads and parses specified HTML code string.
        /// </summary>
        /// <param name="Code">Specifies HTML code string which has to be loaded and parsed.</param>
        public void LoadHtml(string Code)
        {
            this.Parse(Code);
        }
        /// <summary>
        /// Parses specified code to current HTML document.
        /// </summary>
        /// <param name="Content">Specifies string which has to be parsed into current HTML document.</param>
        private void Parse(string Content)
        { 
            //Go throught bro!
            Regex msr = new Regex("<([^<>]*)>");
            MatchCollection m = msr.Matches(Content);
            List<ParsedNode> nodes = new List<ParsedNode>();
            foreach(Match a in m)
            {
                nodes.Add(new ParsedNode(a.Index, a.Groups[1].Value));
            }
            this.Elements = this.manageElements(nodes, false, Content);
        }
        /// <summary>
        /// Recursively manages elements of current HTML document.
        /// </summary>
        /// <param name="nodes">Specifies nodes which are representing part of content of current HTML document.</param>
        /// <param name="isChild">Specifies if is passed content child content of some node.</param>
        /// <param name="Content">Specifies content of some node.</param>
        /// <returns>HTML element collection.</returns>
        private HtmlElements manageElements(List<ParsedNode> nodes, bool isChild, string Content)
        { 
            bool haveRoot = false;
            HtmlElements col = new HtmlElements();
            int i = 0;
            foreach (ParsedNode n in nodes)
            {
                if (haveRoot && !isChild || this.UsedPositions.Contains(n.Position))
                    break;
                if (n.Name.ToLower() == "doctype")
                {
                    HtmlElement el = new HtmlElement();
                    el.Name = "DOCTYPE";
                    el.Type = ElementType.Doctype;
                    el.Attributes = HtmlAttributes.StaticParse(n.Content);
                    col.Add(el);
                }
                else if (n.Type == NodeType.Starting)
                {
                    haveRoot = true;
                    HtmlElement el = new HtmlElement();
                    el.Name = n.Name;
                    el.Attributes = HtmlAttributes.StaticParse(n.Content);
                    int nodeEnd = this.GetNodeEnd(n, nodes);
                    List<ParsedNode> toGive = new List<ParsedNode>();
                    if (nodeEnd != 0 && nodeEnd < nodes.Count)
                    {
                        for (int f = i + 1; f <= nodeEnd - 1; f++)
                        {
                            toGive.Add(nodes[f]);
                        }
                    }
                    el.Type = ElementType.Compound;
                    el.Elements = this.manageElements(toGive, true, Content);
                    el.InnerText = Regex.Replace(Content.Remove(nodes[nodeEnd].Position).Remove(0, n.Position + n.Content.Length + 2), "<.*?>.*?<.*?>|<.*?/>", "");
                    col.Add(el);
                }
                else if (n.Type == NodeType.OneLine)
                {
                    HtmlElement el = new HtmlElement();
                    el.Type = ElementType.Simple;
                    el.Name = n.Name;
                    el.Attributes = HtmlAttributes.StaticParse(n.Content);
                    col.Add(el);
                }
                this.UsedPositions.Add(n.Position);
                i++;
            }
            return col;
        }
        /// <summary>
        /// Gets nodes end.
        /// </summary>
        /// <param name="n">Specifies end whichs end should be searched.</param>
        /// <param name="nodes">Specifies collection of nodes in which should be specified node searched.</param>
        /// <returns>Position of nodes end.</returns>
        private int GetNodeEnd(ParsedNode n, List<ParsedNode> nodes)
        { 
            int res = 0;
            int i = 0;
            int opens = 0;
            int closes = 0;
            foreach (ParsedNode a in nodes)
            {
                if (a.Name == n.Name && a.Type == NodeType.Starting)
                {
                    opens++;
                }
                if (a.Name == n.Name && a.Type == NodeType.Ending)
                {
                    res = i;
                    closes++;
                    if (opens == closes)
                    {
                        break;
                    }
                }
                i++;
            }
            if (opens != closes)
            {
                throw new HtmlInvalidDocumentException("Number of opening tags in current element not equals number of closes.");
            }
            return res;
        }
        /// <summary>
        /// Saves current document on specified path.
        /// </summary>
        /// <param name="Path">Specifies path of file which should be filled.</param>
        public void Save(string Path)
        {
            File.WriteAllText(Path, this.build());
        }
        /// <summary>
        /// Generates refactorized content string of current document.
        /// </summary>
        /// <returns>Generated refactorized content string of current document.</returns>
        private string build()
        {
            string s = "";
            foreach (HtmlElement e in this.Elements)
            {
                s += (e as IRefactorBuildable).Build(0);
            }
            return s;
        }
        /// <summary>
        /// Loads file content from specified path into current document.
        /// </summary>
        /// <param name="Path">Specifies púath of file which must be loaded.</param>
        public override void LoadContent(string Path)
        {
            this.LoadFile(Path);
            base.LoadContent(Path);
        }
    }
}
